/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.gateway.server.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.ObjectUtil;
import com.iwindplus.boot.util.UrlUtil;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.ServerWebExchangeAttributeConstant;
import com.iwindplus.gateway.server.domain.property.GatewayProperty;
import com.iwindplus.mgt.client.system.ApiWhiteListClient;
import jakarta.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;

/**
 * API白名单过滤器.
 *
 * @author zengdegui
 * @since 2020/4/15
 */
@Slf4j
@Component
public class ApiWhiteListFilter implements Ordered, GlobalFilter, ApplicationRunner {

    @Resource
    private GatewayProperty property;

    @Lazy
    @Resource
    private ApiWhiteListClient apiWhiteListClient;

    private volatile List<String> matchUrls = List.of();

    private static final List<String> DEFAULT_IGNORED_APIS = List.of(
        "/v2/api-docs",
        "/v3/api-docs",
        "/oauth2/token",
        "/oauth2/authorize",
        "/oauth2/introspect",
        "/oauth2/revoke",
        "/oauth2/jwks",
        "/oauth2/consent"
    );

    @Override
    public int getOrder() {
        return GatewayConstant.FilterConstant.FILTER_API_WHITE_LIST_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 开关未启用跳过当前过滤器
        if (Boolean.FALSE.equals(this.property.getApiWhiteList().getEnabled())) {
            return chain.filter(exchange);
        }

        if (log.isInfoEnabled()) {
            // 统计执行时间任务
            StopWatch stopWatch = new StopWatch();
            stopWatch.start(ApiWhiteListFilter.class.getSimpleName());

            this.validate(exchange);

            return chain.filter(exchange)
                .doOnTerminate(() -> {
                    stopWatch.stop();
                    log.info(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
                });
        } else {
            this.validate(exchange);

            return chain.filter(exchange);
        }
    }

    private void validate(ServerWebExchange exchange) {
        String requestPath = exchange.getRequest().getPath().value();
        // 白名单校验
        boolean matched = this.checkWhiteList(requestPath);
        if (matched) {
            exchange.getAttributes().putIfAbsent(ServerWebExchangeAttributeConstant.WHITED_FLAG, true);
        }

        log.info("API白名单匹配: {} -> {}", requestPath, matched ? "命中" : "未命中");
    }

    /**
     * 判断路径是否在白名单中
     */
    private boolean checkWhiteList(String path) {
        if (CollUtil.isEmpty(this.matchUrls)) {
            this.refreshWhiteList();
        }

        final boolean match = UrlUtil.isMatchLike(DEFAULT_IGNORED_APIS, path);
        if (Boolean.FALSE.equals(match)) {
            return UrlUtil.isMatch(this.matchUrls, path);
        }
        return match;
    }

    /**
     * 刷新白名单
     */
    public void refreshWhiteList() {

        Set<String> matchSet = new HashSet<>(16);

        List<String> localIgnored = property.getApiWhiteList().getIgnoredApi();
        if (CollUtil.isNotEmpty(localIgnored)) {
            matchSet.addAll(localIgnored);
        }

        // 添加远程白名单
        Mono.justOrEmpty(this.apiWhiteListClient.listApi())
            .retryWhen(Retry.backoff(this.property.getRemoteRetryCount(), this.property.getRemoteRetryPeriod()))
            .subscribe(data -> {
                if (CollUtil.isNotEmpty(data.getBizData())) {
                    matchSet.addAll(data.getBizData());
                }
            });

        if (ObjectUtil.isNotEmpty(matchSet)) {
            this.matchUrls = List.copyOf(matchSet);

            log.info("API白名单刷新成功，当前条目数: {}", matchSet.size());
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Schedulers.boundedElastic().schedule(this::refreshWhiteList);
    }
}
